import sys
import logging
from ctypes import *

from bcat.error import InvalidArgumentError, StdError

from bcat.ffi.error import LoadingError
from bcat.ffi.callback_ptr_wrapper import CallbackPtrWrapper
from bcat.ffi.loader import SharedObjectsLoader

from bcat.util.utility import Utility


class CApi(object):
    def __init__(self):
        if sys.platform.startswith('win'):
            target_name = "sge_api.dll"
        else:
            target_name = "libsge_api.so"

        (self.__loader, self.__target_dll) = self._load_so(target_name)

    def _load_so(self, target_name):
        try:
            loader = SharedObjectsLoader.from_one(target_name).load()
            target_dll = loader.get(target_name)

            self.c_session_write = target_dll.sge_session_write
            # self.c_session_write.argtypes = [c_void_p, c_int, c_char_p, c_uint]
            self.c_session_write.argtypes = [c_void_p, c_int, c_void_p, c_uint]
            self.c_session_write.restype = c_int

            return loader, target_dll
        except LoadingError as e:
            logging.error(f"load dlls:{target_name} error:{e}")
            raise e

    ''' Service Related '''

    class ServiceCallbacks(Structure):
        # declare Callback on_before_run(ctx)
        SERVICE_ON_BEFORE_RUN = CFUNCTYPE(None, c_void_p)
        # declare Callback on_after_stop(ctx)
        SERVICE_ON_AFTER_STOP = CFUNCTYPE(None, c_void_p)

        _fields_ = [
            ('service_on_before_run', SERVICE_ON_BEFORE_RUN),
            ('service_on_after_stop', SERVICE_ON_AFTER_STOP),
        ]

        @staticmethod
        def create(f_before_run, f_after_stop):
            if f_before_run is None and f_after_stop is None:
                return None
            else:
                return CApi.ServiceCallbacks(
                    CApi.ServiceCallbacks.SERVICE_ON_BEFORE_RUN(f_before_run),
                    CApi.ServiceCallbacks.SERVICE_ON_AFTER_STOP(f_after_stop)
                )
        # end def create

    @staticmethod
    def _is_func_or_none(f):
        return f is None or callable(f)

    @staticmethod
    def _check_success(err_code, ctx):
        if err_code == 0:
            return True
        else:
            raise StdError(err_code, ctx)

    @staticmethod
    def _check_bool(value, ctx):
        if value == 1:
            return True
        elif value == 0:
            return False
        else:
            raise StdError(value, ctx)

    # return handle
    def service_create(self, f_before_run, f_after_stop, ctx):
        if not self._is_func_or_none(f_before_run):
            raise TypeError("f_before_run is not [None or function]")

        if not self._is_func_or_none(f_after_stop):
            raise TypeError("f_after_stop is not [None or function]")

        pimpl = CallbackPtrWrapper(self.ServiceCallbacks.create(f_before_run, f_after_stop))

        self.__target_dll.sge_service_create.argtypes = [c_void_p, c_void_p]
        self.__target_dll.sge_service_create.restype = c_void_p
        ptr = self.__target_dll.sge_service_create(pimpl.cbs_byref(), id(ctx) if ctx is not None else None)
        return pimpl.set_ptr(ptr)

    # return True or Exception
    def service_destroy(self, pimpl):
        if not isinstance(pimpl, CallbackPtrWrapper):
            raise TypeError("pimpl must be CallbackPtrWrapper")
        if pimpl.ptr() is None:
            raise InvalidArgumentError("c-handle is None")

        self.__target_dll.sge_service_destroy.argtypes = [c_void_p]
        self.__target_dll.sge_service_destroy.restype = c_int
        ret = self.__target_dll.sge_service_destroy(pimpl.ptr())
        if 0 == ret:
            pimpl.set_ptr(None)
        return self._check_success(ret, "sge_service_destroy")

    # return True or Exception
    def service_run(self, pimpl):
        if not isinstance(pimpl, CallbackPtrWrapper):
            raise TypeError("pimpl must be CallbackPtrWrapper")
        if pimpl.ptr() is None:
            raise InvalidArgumentError("c-handle is None")

        self.__target_dll.sge_service_run.argtypes = [c_void_p]
        self.__target_dll.sge_service_run.restype = c_int
        ret = self.__target_dll.sge_service_run(pimpl.ptr())
        return self._check_success(ret, "sge_service_run")

    # return True or Exception
    def service_is_running(self, pimpl):
        if not isinstance(pimpl, CallbackPtrWrapper):
            raise TypeError("pimpl must be CallbackPtrWrapper")
        if pimpl.ptr() is None:
            raise InvalidArgumentError("c-handle is None")

        self.__target_dll.sge_service_is_running.argtypes = [c_void_p]
        self.__target_dll.sge_service_is_running.restype = c_int
        ret = self.__target_dll.sge_service_is_running(pimpl.ptr())
        return self._check_bool(ret, "sge_service_is_running")

    # return True or Exception
    def service_stop(self, pimpl):
        if not isinstance(pimpl, CallbackPtrWrapper):
            raise TypeError("pimpl must be CallbackPtrWrapper")
        if pimpl.ptr() is None:
            raise InvalidArgumentError("c-handle is None")

        self.__target_dll.sge_service_stop.argtypes = [c_void_p]
        self.__target_dll.sge_service_stop.restype = c_int
        ret = self.__target_dll.sge_service_stop(pimpl.ptr())
        return self._check_success(ret, "sge_service_stop")

    ''' Session Related '''

    class SessionCallbacks(Structure):
        # declare Callback on_connected(ctx)
        SESSION_ON_CONNECTED = CFUNCTYPE(None, c_void_p)
        # declare Callback on_disconnected(error_no, ctx)
        SESSION_ON_DISCONNECTED = CFUNCTYPE(None, c_int, c_void_p)
        # declare Callback on_read(msg_type, msg, msg_len, ctx)
        SESSION_ON_READ = CFUNCTYPE(None, c_int, c_void_p, c_uint, c_void_p)

        _fields_ = [
            ('session_on_read', SESSION_ON_READ),
            ('session_on_connected', SESSION_ON_CONNECTED),
            ('session_on_disconnected', SESSION_ON_DISCONNECTED),
        ]

        @staticmethod
        def create(f_on_connected, f_on_disconnected, f_on_read):
            if f_on_connected is None and f_on_disconnected is None and f_on_read is None:
                return None
            else:
                return CApi.SessionCallbacks(
                    CApi.SessionCallbacks.SESSION_ON_READ(f_on_read),
                    CApi.SessionCallbacks.SESSION_ON_CONNECTED(f_on_connected),
                    CApi.SessionCallbacks.SESSION_ON_DISCONNECTED(f_on_disconnected)
                )
        # end def create

    @staticmethod
    def check_and_set_default_heartbeat_timeout(heartbeat_timeout):
        return 60 if heartbeat_timeout is None or heartbeat_timeout < 25 else heartbeat_timeout

    # return handle
    def session_create(self, service, **kwargs):
        ip = kwargs.get("ip")
        ip = (ip is None) and b"127.0.0.1" or Utility.string_to_bytes(ip)

        port = kwargs.get("port")
        port = port is None and 3467 or port

        connect_time_ms = kwargs.get("connect_time_ms")
        connect_time_ms = (connect_time_ms is None or connect_time_ms == 0) and 2000 or connect_time_ms

        f_on_connected = kwargs.get("f_on_connected")
        f_on_disconnected = kwargs.get("f_on_disconnected")
        f_on_read = kwargs.get("f_on_read")
        pimpl = CallbackPtrWrapper(CApi.SessionCallbacks.create(f_on_connected, f_on_disconnected, f_on_read))

        heartbeat_send_interval = kwargs.get("heartbeat_send_interval")
        heartbeat_send_interval = 20 if (heartbeat_send_interval is None or heartbeat_send_interval <= 0) \
            else heartbeat_send_interval

        heartbeat_timeout = kwargs.get("heartbeat_timeout")
        heartbeat_timeout = self.check_and_set_default_heartbeat_timeout(heartbeat_timeout)

        ctx = kwargs.get("ctx")

        return self._session_create(service, ip, port, connect_time_ms, pimpl, heartbeat_send_interval,
                                    heartbeat_timeout, ctx)

    def _session_create(self, service, ip, port, connect_time_ms, pimpl, heartbeat_send_interval, heartbeat_timeout,
                        ctx):
        if not isinstance(service, CallbackPtrWrapper):
            raise TypeError("service must be CallbackPtrWrapper")

        self.__target_dll.sge_session_create_with_connect_timeout.argtypes = [c_void_p, c_char_p, c_int, c_ulonglong,
                                                                              c_void_p, c_int, c_int, c_void_p]
        self.__target_dll.sge_session_create_with_connect_timeout.restype = c_void_p
        ptr = self.__target_dll.sge_session_create_with_connect_timeout(service.ptr(), ip, port, connect_time_ms,
                                                                        pimpl.cbs_byref(), heartbeat_send_interval,
                                                                        heartbeat_timeout, 
                                                                        id(ctx) if ctx is not None else None)
        return pimpl.set_ptr(ptr)

    # return True or Exception
    def session_destroy(self, pimpl):
        if not isinstance(pimpl, CallbackPtrWrapper):
            raise TypeError("pimpl must be CallbackPtrWrapper")
        if pimpl.ptr() is None:
            raise InvalidArgumentError("c-handle is None")

        self.__target_dll.sge_session_destroy.argtypes = [c_void_p]
        self.__target_dll.sge_session_destroy.restype = c_int
        ret = self.__target_dll.sge_session_destroy(pimpl.ptr())
        return self._check_success(ret, "sge_session_destroy")

    # return True or Exception
    def session_connect(self, pimpl):
        if not isinstance(pimpl, CallbackPtrWrapper):
            raise TypeError("pimpl must be CallbackPtrWrapper")
        if pimpl.ptr() is None:
            raise InvalidArgumentError("c-handle is None")

        self.__target_dll.sge_session_connect.argtypes = [c_void_p]
        self.__target_dll.sge_session_connect.restype = c_int
        ret = self.__target_dll.sge_session_connect(pimpl.ptr())
        return self._check_success(ret, "sge_session_connect")

    # return True or Exception
    def session_disconnect(self, pimpl):
        if not isinstance(pimpl, CallbackPtrWrapper):
            raise TypeError("pimpl must be CallbackPtrWrapper")
        if pimpl.ptr() is None:
            raise InvalidArgumentError("c-handle is None")

        self.__target_dll.sge_session_disconnect.argtypes = [c_void_p]
        self.__target_dll.sge_session_disconnect.restype = c_int
        ret = self.__target_dll.sge_session_disconnect(pimpl.ptr())
        return self._check_success(ret, "sge_session_disconnect")

    # return True or Exception
    def session_is_connected(self, pimpl):
        if not isinstance(pimpl, CallbackPtrWrapper):
            raise TypeError("pimpl must be CallbackPtrWrapper")
        if pimpl.ptr() is None:
            raise InvalidArgumentError("c-handle is None")

        self.__target_dll.sge_session_is_connected.argtypes = [c_void_p]
        self.__target_dll.sge_session_is_connected.restype = c_int
        ret = self.__target_dll.sge_session_is_connected(pimpl.ptr())
        return self._check_bool(ret, "sge_session_is_connected")

    # msg => bytes
    def session_write(self, pimpl, msg):
        return self.__session_write(pimpl, 1, msg)

    # msg => bytes
    def session_write_encrypt(self, pimpl, msg):
        return self.__session_write(pimpl, 4, msg)

    # return True or Exception
    def __session_write(self, pimpl, msg_type, msg):
        if isinstance(pimpl, CallbackPtrWrapper) and pimpl.ptr() is not None:
            ret = self.c_session_write(pimpl.ptr(), msg_type, cast(msg, c_void_p), len(msg))
            return self._check_success(ret, "sge_session_write")
        else:
            raise TypeError("pimpl must be CallbackPtrWrapper, and not None")

    # return string or None
    def session_get_ip(self, pimpl):
        if not isinstance(pimpl, CallbackPtrWrapper):
            raise TypeError("pimpl must be CallbackPtrWrapper")
        if pimpl.ptr() is None:
            raise InvalidArgumentError("c-handle is None")

        self.__target_dll.sge_session_get_ip.argtypes = [c_void_p]
        self.__target_dll.sge_session_get_ip.restype = c_char_p
        return Utility.bytes_to_string(self.__target_dll.sge_session_get_ip(pimpl.ptr()))

    # return int
    def session_get_port(self, pimpl):
        if not isinstance(pimpl, CallbackPtrWrapper):
            raise TypeError("pimpl must be CallbackPtrWrapper")
        if pimpl.ptr() is None:
            raise InvalidArgumentError("c-handle is None")

        self.__target_dll.sge_session_get_port.argtypes = [c_void_p]
        self.__target_dll.sge_session_get_port.restype = c_int
        return self.__target_dll.sge_session_get_port(pimpl.ptr())

    # return int
    def session_get_heartbeat_timeout(self, pimpl):
        if not isinstance(pimpl, CallbackPtrWrapper):
            raise TypeError("pimpl must be CallbackPtrWrapper")
        if pimpl.ptr() is None:
            raise InvalidArgumentError("c-handle is None")

        self.__target_dll.sge_session_get_heartbeat_timeout.argtypes = [c_void_p]
        self.__target_dll.sge_session_get_heartbeat_timeout.restype = c_int
        return self.__target_dll.sge_session_get_heartbeat_timeout(pimpl.ptr())

    # return True or Exception
    def session_set_heartbeat_timeout(self, pimpl, second):
        if not isinstance(pimpl, CallbackPtrWrapper):
            raise TypeError("pimpl must be CallbackPtrWrapper")
        if pimpl.ptr() is None:
            raise InvalidArgumentError("c-handle is None")

        second = self.check_and_set_default_heartbeat_timeout(second)

        self.__target_dll.sge_session_set_heartbeat_timeout.argtypes = [c_void_p, c_uint]
        self.__target_dll.sge_session_set_heartbeat_timeout.restype = c_int
        ret = self.__target_dll.sge_session_set_heartbeat_timeout(pimpl.ptr(), second)
        return self._check_success(ret, "sge_session_set_heartbeat_timeout:" + str(second))

    ''' Utility Related '''

    LOG_LEVEL_DEBUG = 1
    LOG_LEVEL_TRACE = 2
    LOG_LEVEL_NOTICE = 3
    LOG_LEVEL_WARNING = 64
    LOG_LEVEL_ERROR = 65
    LOG_LEVEL_FATAL = 66

    @staticmethod
    def level_is_valid(level):
        return level is not None and level in (
            CApi.LOG_LEVEL_DEBUG, CApi.LOG_LEVEL_TRACE, CApi.LOG_LEVEL_NOTICE, CApi.LOG_LEVEL_WARNING,
            CApi.LOG_LEVEL_ERROR,
            CApi.LOG_LEVEL_FATAL)

    # return handle
    def log_create(self, directory, name, level):
        directory = b"." if directory is None else Utility.string_to_bytes(directory)
        name = b"__api__" if name is None or name.isspace() else Utility.string_to_bytes(name)
        level = 1 if not self.level_is_valid(level) else level

        self.__target_dll.sge_mthread_log_create.argtypes = [c_char_p, c_char_p, c_int]
        self.__target_dll.sge_mthread_log_create.restype = c_void_p
        return self.__target_dll.sge_mthread_log_create(directory, name, level)

    # return True or Exception
    def log_destroy(self, ptr):
        if ptr is None:
            raise InvalidArgumentError("c-handle is None")

        self.__target_dll.sge_mthread_log_destroy.argtypes = [c_void_p]
        self.__target_dll.sge_mthread_log_destroy.restype = c_int
        ret = self.__target_dll.sge_mthread_log_destroy(ptr)
        return self._check_success(ret, "sge_mthread_log_destroy")

    # return True or Exception
    def log_set_level(self, ptr, level):
        if ptr is None:
            raise InvalidArgumentError("c-handle of log is None")

        if not self.level_is_valid(level):
            raise InvalidArgumentError("level is invalid")

        self.__target_dll.sge_mthread_log_set_level.argtypes = [c_void_p, c_int]
        self.__target_dll.sge_mthread_log_set_level.restype = c_int
        ret = self.__target_dll.sge_mthread_log_set_level(ptr, level)
        return self._check_success(ret, "sge_mthread_log_set_level")

    # return string or None
    def log_get_dir(self, ptr):
        if ptr is None:
            raise InvalidArgumentError("c-handle of log is None")

        self.__target_dll.sge_mthread_log_get_dir.argtypes = [c_void_p]
        self.__target_dll.sge_mthread_log_get_dir.restype = c_char_p
        return Utility.bytes_to_string(self.__target_dll.sge_mthread_log_get_dir(ptr))

    # return string or None
    def log_get_name(self, ptr):
        if ptr is None:
            raise InvalidArgumentError("c-handle of log is None")

        self.__target_dll.sge_mthread_log_get_name.argtypes = [c_void_p]
        self.__target_dll.sge_mthread_log_get_name.restype = c_char_p
        return Utility.bytes_to_string(self.__target_dll.sge_mthread_log_get_name(ptr))

    # return string or None
    def log_get_path(self, ptr):
        if ptr is None:
            raise InvalidArgumentError("c-handle of log is None")

        self.__target_dll.sge_mthread_log_get_path.argtypes = [c_void_p]
        self.__target_dll.sge_mthread_log_get_path.restype = c_char_p
        return Utility.bytes_to_string(self.__target_dll.sge_mthread_log_get_path(ptr))

    # return True or Exception
    def log_enable_date_roll(self, ptr):
        if ptr is None:
            raise InvalidArgumentError("c-handle of log is None")

        self.__target_dll.sge_mthread_log_set_date_roll.argtypes = [c_void_p]
        self.__target_dll.sge_mthread_log_set_date_roll.restype = c_int
        ret = self.__target_dll.sge_mthread_log_set_date_roll(ptr)
        return self._check_success(ret, "sge_mthread_log_set_date_roll")

    # return True or Exception
    def log_disable_date_roll(self, ptr):
        if ptr is None:
            raise InvalidArgumentError("c-handle of log is None")

        self.__target_dll.sge_mthread_log_unset_date_roll.argtypes = [c_void_p]
        self.__target_dll.sge_mthread_log_unset_date_roll.restype = c_int
        ret = self.__target_dll.sge_mthread_log_unset_date_roll(ptr)
        return self._check_success(ret, "sge_mthread_log_unset_date_roll")

    # return True or Exception
    def log_enable_size_roll(self, ptr, max_file_size_m):
        if ptr is None:
            raise InvalidArgumentError("c-handle of log is None")

        if max_file_size_m is None or max_file_size_m <= 0:
            max_file_size_m = 100

        self.__target_dll.sge_mthread_log_set_size_roll.argtypes = [c_void_p, c_int]
        self.__target_dll.sge_mthread_log_set_size_roll.restype = c_int
        ret = self.__target_dll.sge_mthread_log_set_size_roll(ptr, max_file_size_m)
        return self._check_success(ret, "sge_mthread_log_set_size_roll")

    # return True or Exception
    def log_disable_size_roll(self, ptr):
        if ptr is None:
            raise InvalidArgumentError("c-handle of log is None")

        self.__target_dll.sge_mthread_log_unset_size_roll.argtypes = [c_void_p]
        self.__target_dll.sge_mthread_log_unset_size_roll.restype = c_int
        ret = self.__target_dll.sge_mthread_log_unset_size_roll(ptr)
        return self._check_success(ret, "sge_mthread_log_unset_size_roll")

    # return string
    def get_version(self):
        self.__target_dll.sge_get_version.restype = c_char_p
        return Utility.bytes_to_string(self.__target_dll.sge_get_version())


class CApiInstance:
    __instance = None

    @staticmethod
    def get():
        if CApiInstance.__instance is None:
            CApiInstance.__instance = CApi()
        return CApiInstance.__instance
